import * as ts from 'typescript';
import AstPrinterBase from '../AstPrinterBase';
import * as cs from '../csharp/CSharpAst';
import type KotlinEmitterContext from './KotlinEmitterContext';

export default class KotlinAstPrinter extends AstPrinterBase {
    private _forceInteger: boolean = false;
    private _returnRunTest: boolean[] = [];
    private _thisScope: string[] = [];
    private _useScopes: number[] = [];

    protected override _context: KotlinEmitterContext;

    public constructor(sourceFile: cs.SourceFile, context: KotlinEmitterContext) {
        super(sourceFile, context);
        this._context = context;
    }

    private _keywords: Set<string> = new Set<string>([
        'as',
        'as?',
        'break',
        'class',
        'continue',
        'do',
        'else',
        'false',
        'for',
        'fun',
        'if',
        'in',
        '!in',
        'interface',
        'is',
        '!is',
        'null',
        'object',
        'package',
        'return',
        'super',
        'this',
        'throw',
        'true',
        'try',
        'typealias',
        'typeof',
        'val',
        'var',
        'when',
        'while',

        'abstract',
        'actual',
        'annotation',
        'companion',
        'const',
        'crossinline',
        'data',
        'enum',
        'expect',
        'external',
        'final',
        'infix',
        'inline',
        'inner',
        'internal',
        'lateinit',
        'noinline',
        'open',
        'operator',
        'out',
        'override',
        'private',
        'protected',
        'public',
        'reified',
        'sealed',
        'suspend',
        'tailrec',
        'vararg'
    ]);

    protected override escapeIdentifier(identifier: string): string {
        if (this._keywords.has(identifier)) {
            return `\`${identifier}\``;
        }
        return identifier;
    }

    protected writeSourceFile(sourceFile: cs.SourceFile) {
        this.writeLine('// <auto-generated>');
        this.writeLine('// This code was auto-generated.');
        this.writeLine('// Changes to this file may cause incorrect behavior and will be lost if');
        this.writeLine('// the code is regenerated.');
        this.writeLine('// </auto-generated>');
        this.writeLine();
        this.writeLine('@file:Suppress(');
        this.writeLine('    "KotlinRedundantDiagnosticSuppress",');
        this.writeLine('    "RedundantVisibilityModifier",');
        this.writeLine('    "RedundantExplicitType",');
        this.writeLine('    "RedundantUnitReturnType",');
        this.writeLine('    "RemoveRedundantQualifierName",');
        this.writeLine('    "RemoveExplicitTypeArguments",');
        this.writeLine('    "MemberVisibilityCanBePrivate",');
        this.writeLine('    "MoveLambdaOutsideParentheses",');
        this.writeLine('    "ConvertSecondaryConstructorToPrimary",');
        this.writeLine('    "RemoveRedundantCallsOfConversionMethods",');
        this.writeLine('    "MayBeConstant",');
        this.writeLine('    "UnusedImport",');
        this.writeLine('    "CanBeVal",');
        this.writeLine('    "CascadeIf",');
        this.writeLine('    "unused",');
        this.writeLine('    "LocalVariableName",');
        this.writeLine('    "NON_EXHAUSTIVE_WHEN",');
        this.writeLine('    "UNCHECKED_CAST",');
        this.writeLine('    "USELESS_CAST",');
        this.writeLine('    "DEPRECATION",');
        this.writeLine('    "PARAMETER_NAME_CHANGED_ON_OVERRIDE",');
        this.writeLine('    "UNNECESSARY_NOT_NULL_ASSERTION",');
        this.writeLine('    "UNNECESSARY_SAFE_CALL",');
        this.writeLine('    "UNUSED_ANONYMOUS_PARAMETER",');
        this.writeLine('    "UNUSED_PARAMETER",');
        this.writeLine('    "UNUSED_VALUE",');
        this.writeLine('    "UNREACHABLE_CODE",');
        this.writeLine('    "REDUNDANT_ELSE_IN_WHEN",');
        this.writeLine('    "VARIABLE_WITH_REDUNDANT_INITIALIZER"');
        this.writeLine(')');
        this.writeLine(`package ${sourceFile.namespace.namespace}`);

        for (const using of sourceFile.usings) {
            this.writeImport(using);
        }
        if (sourceFile.usings.length > 0) {
            this.writeLine();
        }

        this.writeNamespaceMembers(sourceFile.namespace.declarations);
    }

    protected writeDocumentation(d: cs.DocumentedElement) {
        if (d.documentation) {
            this.writeLine('/**');
            this.writeDocumentationLines(d.documentation, true);
            if (d.nodeType !== cs.SyntaxKind.MethodDeclaration) {
                this.writeLine(' */');
            }
        }
    }

    protected writeParameterDocumentation(d: cs.MethodDeclaration) {
        if (d.parameters.length > 0) {
            if (!d.documentation) {
                this.writeLine('/**');
            }

            for (const p of d.parameters) {
                if (p.documentation) {
                    this.write(' * @param ');
                    this.writeIdentifier(p.name);
                    this.write(' ');
                    this.writeDocumentationLines(p.documentation, false);
                    this.writeLine();
                }
            }
            this.writeLine(' */');
        } else if (d.documentation) {
            this.writeLine(' */');
        }
    }

    protected writeDocumentationLines(documentation: string, multiLine: boolean) {
        const lines = documentation.split('\n');
        if (lines.length > 1 || multiLine) {
            if (!this._isStartOfLine) {
                this.writeLine();
            }
            for (const line of lines) {
                this.writeLine(` * ${line.replaceAll('/*', '/\\*')}`);
            }
        } else if (lines.length === 1) {
            if (this._isStartOfLine) {
                this.writeLine(` * ${lines[0]}`);
            } else {
                this.write(lines[0]);
            }
        }
    }

    protected writeParameter(p: cs.ParameterDeclaration) {
        if (p.params) {
            this.write('vararg ');
        }

        this.writeIdentifier(p.name);
        if (p.type) {
            this.write(': ');
            if (p.params) {
                this.writeType((p.type as cs.ArrayTypeNode).elementType, false, false, false, true);
            } else {
                this.writeType(p.type, false, false, false, true);
            }
        }
        if (!this.isOverrideMethod(p.parent!)) {
            if (p.initializer) {
                this.write(' = ');
                this.writeExpression(p.initializer);
            } else if (p.type && p.isOptional) {
                let type: cs.TypeNode = p.type;
                if (cs.isTypeReference(p.type)) {
                    type = p.type.reference as cs.TypeNode;
                }
                if (cs.isPrimitiveTypeNode(type)) {
                    switch ((type as cs.PrimitiveTypeNode).type) {
                        case cs.PrimitiveType.Bool:
                            this.write(' = false');
                            break;
                        case cs.PrimitiveType.Double:
                            this.write(' = 0.0');
                            break;
                        case cs.PrimitiveType.Int:
                            this.write(' = 0');
                            break;
                        default:
                            this.write(' = null');
                            break;
                    }
                } else {
                    this.write(' = null');
                }
            }
        }
    }

    protected isOverrideMethod(parent: cs.Node) {
        if (parent.nodeType !== cs.SyntaxKind.MethodDeclaration) {
            return false;
        }

        const method = parent as cs.MethodDeclaration;
        return method.isOverride;
    }

    protected writeInterfaceDeclaration(d: cs.InterfaceDeclaration) {
        this.writeDocumentation(d);

        this.writeLine('@kotlin.contracts.ExperimentalContracts');
        this.writeLine('@kotlin.ExperimentalUnsignedTypes');
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);
        this.write('interface ');
        this.writeIdentifier(d.name);
        this.writeTypeParameters(d.typeParameters);

        if (d.interfaces && d.interfaces.length > 0) {
            this.write(': ');
            this.writeCommaSeparated(d.interfaces, i => this.writeType(i));
        }

        this.writeTypeParameterConstraints(d.typeParameters);
        this.writeLine();
        this.beginBlock();
        for (const m of d.members) {
            this.writeMember(m);
        }

        this.endBlock();
    }

    protected writeEnumDeclaration(d: cs.EnumDeclaration) {
        this._forceInteger = true;
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);
        this.write('enum class ');
        this.writeIdentifier(d.name);
        this.write('(override val value: Int): alphaTab.core.IAlphaTabEnum');
        this.writeLine();
        this.beginBlock();

        let currentEnumValue = 0;

        for (let i = 0; i < d.members.length; i++) {
            const m = d.members[i];
            this.writeDocumentation(m);
            this.writeAttributes(d);
            this.writeIdentifier(m.name);
            if (m.initializer) {
                this.write('(');
                this.writeExpression(m.initializer);
                this.write(')');
            } else {
                this.write('(');
                this.write(currentEnumValue.toString());
                this.write(')');
            }

            if (i < d.members.length - 1) {
                this.writeLine(',');
            } else {
                this.writeLine(';');
            }

            currentEnumValue++;
        }

        this.write('companion object : alphaTab.core.IAlphaTabEnumCompanion<');
        this.write(d.name);
        this.write('>');

        this.beginBlock();

        this.writeLine(`public override val values = arrayOf(${d.members.map(m => m.name).join(', ')}) `);
        this.write('public override fun fromValue(v:Double): ');
        this.writeIdentifier(d.name);
        this.beginBlock();

        this.write('return when(v.toInt())');
        this.beginBlock();

        for (const m of d.members) {
            this.writeIdentifier(m.name);
            this.write('.value -> ');
            this.writeIdentifier(m.name);
            this.writeLine();
        }
        this.write('else -> throw ClassCastException("No enum with value $v found")');
        this.endBlock();

        this.endBlock();

        this.endBlock();

        this.endBlock();
        this._forceInteger = false;
    }

    protected writeClassDeclaration(d: cs.ClassDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeLine('@kotlin.contracts.ExperimentalContracts');
        this.writeLine('@kotlin.ExperimentalUnsignedTypes');
        this.writeVisibility(d.visibility);

        if (d.isAbstract) {
            this.write('abstract ');
        } else if (d.hasVirtualMembersOrSubClasses) {
            this.write('open ');
        }

        this.write('class ');
        this.writeIdentifier(d.name);

        this.writeTypeParameters(d.typeParameters);

        if (d.baseClass) {
            this.write(': ');
            this.writeType(d.baseClass);
        }

        if (d.interfaces && d.interfaces.length > 0) {
            if (d.baseClass) {
                this.write(', ');
            } else {
                this.write(': ');
            }

            this.writeCommaSeparated(d.interfaces, i => this.writeType(i));
        }
        this.writeTypeParameterConstraints(d.typeParameters);

        this.writeLine();

        this.beginBlock();

        let hasConstuctor = false;
        const statics: cs.ClassMember[] = [];
        for (const m of d.members) {
            if ('isStatic' in m && m.isStatic) {
                statics.push(m);
            } else {
                this.writeMember(m);
                if (cs.isConstructorDeclaration(m) && !m.isStatic) {
                    hasConstuctor = true;
                }
            }
        }

        if (statics.length > 0) {
            this.write('companion object');
            this.beginBlock();

            for (const s of statics) {
                this.writeMember(s);
            }

            this.endBlock();
        }

        if (d.baseClass && !hasConstuctor) {
            let baseClass: cs.TypeReferenceType | undefined = d;
            let constructorDeclaration: cs.ConstructorDeclaration | undefined = undefined;
            while (baseClass && !constructorDeclaration) {
                if (typeof baseClass === 'string') {
                    constructorDeclaration = undefined;
                    break;
                }
                if (cs.isClassDeclaration(baseClass)) {
                    constructorDeclaration = baseClass.members.find(m =>
                        cs.isConstructorDeclaration(m)
                    ) as cs.ConstructorDeclaration;
                    if (constructorDeclaration) {
                        break;
                    }

                    baseClass =
                        baseClass.baseClass && cs.isTypeReference(baseClass.baseClass)
                            ? baseClass.baseClass.reference
                            : undefined;
                } else {
                    constructorDeclaration = undefined;
                    break;
                }
            }

            if (constructorDeclaration) {
                const defaultConstructor = {
                    parent: d,
                    name: '',
                    nodeType: cs.SyntaxKind.ConstructorDeclaration,
                    isStatic: false,
                    parameters: [],
                    visibility: cs.Visibility.Public,
                    body: {
                        parent: null,
                        nodeType: cs.SyntaxKind.Block,
                        statements: [],
                        tsNode: d.tsNode
                    } as cs.Block,
                    tsNode: d.tsNode,
                    baseConstructorArguments: []
                } as cs.ConstructorDeclaration;
                defaultConstructor.body!.parent = defaultConstructor;
                defaultConstructor.parameters = constructorDeclaration.parameters;
                defaultConstructor.baseConstructorArguments = constructorDeclaration.parameters.map(
                    p =>
                        ({
                            parent: defaultConstructor,
                            nodeType: cs.SyntaxKind.Identifier,
                            text: p.name,
                            tsNode: defaultConstructor.tsNode
                        }) as cs.Identifier
                );
                this.writeMember(defaultConstructor);
            } else {
                this.writeLine('public constructor()');
                if (d.baseClass) {
                    this.writeLine(': super()');
                }
            }
        } else if (!hasConstuctor) {
            this.writeLine('public constructor()');
            if (d.baseClass) {
                this.writeLine(': super()');
            }
        }

        this.endBlock();
    }

    protected writeAttribute(a: cs.Attribute): void {
        this.write('@');
        this.writeType(a.type);
        if (a.arguments && a.arguments.length > 0) {
            this.write('(');
            this.writeCommaSeparated(a.arguments!, x => this.writeExpression(x));
            this.write(')');
        }
        this.writeLine();
    }

    protected writeMethodDeclaration(d: cs.MethodDeclaration) {
        this._returnRunTest.push(d.isTestMethod);
        this.writeDocumentation(d);
        this.writeParameterDocumentation(d);
        this.writeAttributes(d);
        if (d.isStatic) {
            this.writeLine('@kotlin.jvm.JvmStatic');
        }
        if (d.isTestMethod) {
            this.writeLine('@org.junit.Test');
        }
        this.writeVisibility(d.visibility);

        if (d.isAbstract) {
            this.write('abstract ');
        } else if (d.isVirtual) {
            this.write('open ');
        }

        if (d.isOverride) {
            this.write('override ');
        }

        const suspend = d.isAsync && !d.isTestMethod;
        if (suspend) {
            this.write('suspend ');
        }

        this.write('fun ');
        this.writeTypeParameters(d.typeParameters);
        this.writeIdentifier(d.name);
        this.writeParameters(d.parameters);

        if (d.isTestMethod) {
            this.write(' = runTest');
        } else {
            this.write(': ');
            this.writeType(d.returnType, undefined, undefined, undefined, !suspend);
            this.writeTypeParameterConstraints(d.typeParameters);
        }

        if (d.isGeneratorFunction) {
            this.write(' = iterator');
            this._thisScope.push((d.parent as cs.NamedTypeDeclaration).name);
        }
        this.writeBody(d.body);
        if (d.isGeneratorFunction) {
            this._thisScope.pop();
        }

        this._returnRunTest.pop();
    }

    protected override writeThisLiteral(expr: cs.ThisLiteral): void {
        super.writeThisLiteral(expr);
        const scope = this._thisScope[this._thisScope.length - 1];
        if (scope) {
            this.write(`@${scope}`);
        }
    }

    protected writeBody(body: cs.Expression | cs.Block | undefined) {
        if (body) {
            if (cs.isBlock(body)) {
                this.writeBlock(body as cs.Block);
            } else {
                this.write(' = ');
                this.writeExpression(body as cs.Expression);
                this.writeLine();
            }
        } else {
            this.writeLine();
        }
    }

    protected writeConstructorDeclaration(d: cs.ConstructorDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);
        if (d.isStatic) {
            this.write('init ');
        } else {
            this.write('constructor');
            this.writeParameters(d.parameters);
            if (d.baseConstructorArguments) {
                this.writeLine();
                this._indent++;
                this.write(': super(');
                this.writeCommaSeparated(d.baseConstructorArguments, e => this.writeExpression(e));
                this.write(')');
                this._indent--;
            }
        }

        if (d.body?.nodeType !== cs.SyntaxKind.Block || (d.body as cs.Block).statements.length > 0) {
            this.writeBody(d.body);
        }
    }

    protected writePropertyDeclaration(d: cs.PropertyDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);

        if (d.isStatic) {
            this.writeLine('@kotlin.jvm.JvmStatic');
        }

        this.writeVisibility(d.visibility);

        const isAutoProperty = this.isAutoProperty(d);
        const isLateInit = this.isLateInit(d);

        const writeAsField = this.writePropertyAsField(d);

        if (d.isVirtual && !d.isAbstract) {
            this.write('open ');
        }

        if (writeAsField && this.canBeConstant(d)) {
            this.write('val ');
        } else {
            if (d.isAbstract) {
                this.write('abstract ');
            }

            if (d.isOverride) {
                this.write('override ');
            }

            if (isLateInit) {
                this.write('lateinit ');
            }

            if (
                (!isAutoProperty && !d.setAccessor) ||
                (d.isAbstract && !d.setAccessor) ||
                (cs.isInterfaceDeclaration(d.parent!) && !d.setAccessor)
            ) {
                this.write('val ');
            } else {
                this.write('var ');
            }
        }

        this.writeIdentifier(d.name);
        this.write(': ');
        this.writeType(d.type);

        const needsInitializer =
            isAutoProperty && d.type.isNullable && d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration;

        let initializerWritten = false;
        if (d.initializer && !isLateInit) {
            this.write(' = ');
            this.writeExpression(d.initializer);
            initializerWritten = true;
            this.writeLine();
        } else if (writeAsField) {
            if (needsInitializer && !initializerWritten) {
                this.write(' = null');
            }
            this.writeLine();
        }

        if (!writeAsField) {
            if (needsInitializer && !initializerWritten) {
                this.write(' = null');
            }
            this.writeLine();

            if (!isAutoProperty) {
                if (d.getAccessor) {
                    this.writePropertyAccessor(d.getAccessor);
                }

                if (d.setAccessor) {
                    this.writePropertyAccessor(d.setAccessor);
                }
            }
        }
    }

    protected isLateInit(d: cs.PropertyDeclaration) {
        return d.initializer && cs.isNonNullExpression(d.initializer) && cs.isNullLiteral(d.initializer.expression);
    }

    protected isAutoProperty(d: cs.PropertyDeclaration) {
        if (d.getAccessor && d.getAccessor.body) {
            return false;
        }
        if (d.setAccessor && d.setAccessor.body) {
            return false;
        }
        return true;
    }

    protected override writeReturnStatement(r: cs.ReturnStatement): void {
        if (this._returnRunTest[this._returnRunTest.length - 1]) {
            this.writeLine('return@runTest');
        } else {
            super.writeReturnStatement(r);
        }
    }

    protected writePropertyAccessor(accessor: cs.PropertyAccessorDeclaration) {
        this.write(accessor.keyword);
        if (accessor.keyword === 'set') {
            this.write('(value)');
        } else {
            this.write('()');
        }
        this.writeBody(accessor.body);
    }

    protected writeFieldDeclarat1on(d: cs.FieldDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        if (this._context.isConst(d)) {
            this.write('const ');
        } else {
            if (d.isReadonly) {
                this.write('readonly ');
            }
        }

        this.writeType(d.type);
        this.write(' ');
        this.writeIdentifier(d.name);
        if (d.initializer) {
            this.write(' = ');
            this.writeExpression(d.initializer);
        }
        this.writeLine();
    }

    protected writeType(
        type: cs.TypeNode,
        forNew: boolean = false,
        asNativeArray: boolean = false,
        forTypeConstraint: boolean = false,
        allowPromise: boolean = true
    ) {
        switch (type.nodeType) {
            case cs.SyntaxKind.PrimitiveTypeNode:
                if (forTypeConstraint) {
                    switch ((type as cs.PrimitiveTypeNode).type) {
                        case cs.PrimitiveType.Bool:
                        case cs.PrimitiveType.Int:
                        case cs.PrimitiveType.Double:
                            this.write('IAlphaTabEnum');
                            break;
                        case cs.PrimitiveType.Object:
                        case cs.PrimitiveType.String:
                        case cs.PrimitiveType.Void:
                            this.write('class');
                            break;
                    }
                } else {
                    switch ((type as cs.PrimitiveTypeNode).type) {
                        case cs.PrimitiveType.Bool:
                            this.write('Boolean');
                            break;
                        case cs.PrimitiveType.Double:
                            this.write(this._forceInteger ? 'Int' : 'Double');
                            break;
                        case cs.PrimitiveType.Int:
                            this.write('Int');
                            break;
                        case cs.PrimitiveType.Object:
                            this.write('Any');
                            break;
                        case cs.PrimitiveType.String:
                            this.write('String');
                            break;
                        case cs.PrimitiveType.Void:
                            this.write('Unit');
                            break;
                        case cs.PrimitiveType.Var:
                            this.write('var');
                            break;
                        case cs.PrimitiveType.Long:
                            this.write('Long');
                            break;
                    }
                }

                break;
            case cs.SyntaxKind.ArrayTypeNode:
                const arrayType = type as cs.ArrayTypeNode;
                if (asNativeArray) {
                    this.write('Array<');
                    this.writeType(arrayType.elementType);
                    this.write('>');
                } else {
                    const elementTypeName = this._getContainerTypeName(arrayType.elementType);
                    if (elementTypeName) {
                        this.write('alphaTab.collections.');
                        this.write(elementTypeName);
                        this.write('List');
                    } else {
                        this.write('alphaTab.collections.List<');
                        this.writeType(arrayType.elementType);
                        this.write('>');
                    }
                }

                break;
            case cs.SyntaxKind.MapTypeNode:
                const mapType = type as cs.MapTypeNode;
                if (!mapType.keyType && !mapType.valueType) {
                    this.write('alphaTab.collections.MapBase<*,*>');
                } else {
                    const keyTypeName = this._getContainerTypeName(mapType.keyType!);
                    const valueTypeName = this._getContainerTypeName(mapType.valueType!);

                    this.write('alphaTab.collections.');
                    if (keyTypeName && valueTypeName) {
                        this.write(keyTypeName);
                        this.write(valueTypeName);
                        this.write('Map');
                    } else if (keyTypeName) {
                        this.write(keyTypeName);
                        this.write('ObjectMap<');
                        this.writeType(mapType.valueType!);
                        this.write('>');
                    } else if (valueTypeName) {
                        this.write('Object');
                        this.write(valueTypeName);
                        this.write('Map<');
                        this.writeType(mapType.keyType!);
                        this.write('>');
                    } else {
                        this.write('Map<');
                        this.writeType(mapType.keyType!);
                        this.write(', ');
                        this.writeType(mapType.valueType!);
                        this.write('>');
                    }
                }
                break;
            case cs.SyntaxKind.ArrayTupleNode:
                const arrayTupleType = type as cs.ArrayTupleNode;

                if (arrayTupleType.types.length > 2) {
                    if (forNew) {
                        this.write('alphaTab.core.ArrayTuple');
                    } else {
                        this.write('alphaTab.core.IArrayTuple');
                    }
                    this.write(arrayTupleType.types.length.toString());
                    this.write('<');
                    this.writeCommaSeparated(arrayTupleType.types, p => this.writeType(p));
                } else {
                    let arrayTupleName = '';
                    const newTypeArgs: cs.TypeNode[] = [];
                    for (const arg of arrayTupleType.types) {
                        let itemType: cs.TypeReferenceType = arg;
                        while (typeof itemType !== 'string' && cs.isTypeReference(itemType)) {
                            itemType = itemType.reference;
                        }

                        if (typeof itemType === 'string') {
                            arrayTupleName += 'Object';
                            newTypeArgs.push(arg);
                        } else if (cs.isPrimitiveTypeNode(itemType)) {
                            switch (itemType.type) {
                                case cs.PrimitiveType.Bool:
                                    arrayTupleName += 'Boolean';
                                    break;
                                case cs.PrimitiveType.Int:
                                    arrayTupleName += 'Int';
                                    break;
                                case cs.PrimitiveType.Double:
                                    arrayTupleName += 'Double';
                                    break;
                                default:
                                    arrayTupleName += 'Object';
                                    newTypeArgs.push(arg);
                                    break;
                            }
                        } else {
                            arrayTupleName += 'Object';
                            newTypeArgs.push(arg);
                        }
                    }

                    if (arrayTupleName === 'ObjectObject') {
                        arrayTupleName = '';
                    }

                    if (forNew) {
                        this.write(`alphaTab.core.${arrayTupleName}ArrayTuple`);
                    } else {
                        this.write(`alphaTab.core.I${arrayTupleName}ArrayTuple`);
                    }

                    if (newTypeArgs.length > 0) {
                        this.write('<');
                        this.writeCommaSeparated(newTypeArgs, p => this.writeType(p));
                        this.write('>');
                    }
                }
                break;
            case cs.SyntaxKind.FunctionTypeNode:
                const functionType = type as cs.FunctionTypeNode;
                this.write('(');
                this.write('(');
                if (functionType.parameterTypes.length > 0) {
                    let i = 1;
                    this.writeCommaSeparated(functionType.parameterTypes, p => {
                        this.write(`arg${i++}: `);
                        this.writeType(p);
                    });
                }
                this.write(') -> ');
                this.writeType(functionType.returnType);
                this.write(')');
                break;
            case cs.SyntaxKind.TypeReference:
                const typeReference = type as cs.TypeReference;

                const targetType = (type as cs.TypeReference).reference;
                let typeArguments = typeReference.typeArguments;
                if (typeof targetType === 'string') {
                    if (forNew && targetType === this._context.makeIterableType() && typeArguments) {
                        this.write('alphaTab.collections.SimpleIterable<');
                        this.writeType(typeArguments[0]);
                        this.write('>');
                        typeArguments = undefined;
                    } else {
                        this.write(targetType);
                    }
                } else {
                    if (typeReference.isAsync && allowPromise) {
                        this.write('kotlinx.coroutines.Deferred<');
                    }
                    this.writeType(targetType, forNew);
                }

                if (typeArguments && typeArguments.length > 0) {
                    this.write('<');

                    this.writeCommaSeparated(typeArguments, p => this.writeType(p));

                    this.write('>');
                } else if (typeReference.tsSymbol) {
                    // we have to resolve the number of potential type parameters of the type
                    const expectedParameters = typeReference.tsSymbol.declarations
                        ?.map(d => {
                            if (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) {
                                return d.typeParameters;
                            }
                            return undefined;
                        })
                        .find(x => !!x);

                    if (expectedParameters) {
                        this.write('<');
                        this.writeCommaSeparated(Array.from(expectedParameters), () => this.write('*'));
                        this.write('>');
                    }
                }

                if (typeReference.isAsync && allowPromise) {
                    this.write('>');
                }

                break;
            case cs.SyntaxKind.ClassDeclaration:
            case cs.SyntaxKind.InterfaceDeclaration:
            case cs.SyntaxKind.EnumDeclaration:
            case cs.SyntaxKind.DelegateDeclaration:
                this.write(this._context.getFullName(type as cs.NamedTypeDeclaration));
                break;
            case cs.SyntaxKind.UsingDeclaration:
                this.write((type as cs.UsingDeclaration).name);
                break;
            case cs.SyntaxKind.TypeParameterDeclaration:
                this.writeIdentifier((type as cs.TypeParameterDeclaration).name);
                break;
            case cs.SyntaxKind.EnumMember:
                this.write(this._context.getFullName((type as cs.EnumMember).parent as cs.NamedTypeDeclaration));
                break;
            default:
                this.write(`TODO: ${cs.SyntaxKind[type.nodeType]}`);
                break;
        }
        if (type.isNullable && !forNew && !forTypeConstraint) {
            this.write('?');
        }
    }

    private _getContainerTypeName(type: cs.TypeNode): string | null {
        switch (type.nodeType) {
            case cs.SyntaxKind.PrimitiveTypeNode:
                if (type.isNullable) {
                    return null;
                }
                switch ((type as cs.PrimitiveTypeNode).type) {
                    case cs.PrimitiveType.Bool:
                        return 'Boolean';
                    case cs.PrimitiveType.Int:
                        return 'Int';
                    case cs.PrimitiveType.Double:
                        return 'Double';
                }
                break;
        }
        return null;
    }

    protected writeTypeOfExpression(expr: cs.TypeOfExpression) {
        if (expr.expression) {
            this.writeExpression(expr.expression);
        }
        if (expr.type) {
            this.writeType(expr.type);
        }

        this.write('::class');
    }

    protected writePrefixUnaryExpression(expr: cs.PrefixUnaryExpression) {
        switch (expr.operator) {
            case '~':
                this.write('(');
                this.writeExpression(expr.operand);
                this.write(').toInt().inv()');

                let parent = expr.parent!;
                while (cs.isParenthesizedExpression(parent)) {
                    parent = parent.parent!;
                }

                if (!this.isIntResultExpression(parent)) {
                    this.write('.toDouble()');
                }
                break;
            default:
                this.write(expr.operator);
                this.writeExpression(expr.operand);
                break;
        }
    }

    protected writeBaseLiteralExpression(_expr: cs.BaseLiteralExpression) {
        this.write('super');
    }

    protected writeAwaitExpression(expr: cs.AwaitExpression) {
        this.writeExpression(expr.expression);
    }

    protected writeBinaryExpression(expr: cs.BinaryExpression) {
        this.writeExpression(expr.left);
        this.write(' ');
        switch (expr.operator) {
            case '??':
                if (cs.isNullLiteral(expr.right)) {
                    return;
                }
                this.write('?:');
                break;
            case '??=':
                this.write('= ');
                this.writeExpression(expr.left);
                this.write('?:');
                break;
            case '&':
                this.write('and');
                break;
            case '|':
                this.write('or');
                break;
            case '^':
                this.write('xor');
                break;
            case '<<':
                this.write('shl');
                break;
            case '>>':
                this.write('shr');
                break;
            default:
                this.write(expr.operator);
                break;
        }
        this.write(' ');
        this.writeExpression(expr.right);
    }

    protected writeConditionalExpression(expr: cs.ConditionalExpression) {
        this.write('if(');
        this.writeExpression(expr.condition);
        this.write(')  ');
        this.writeExpression(expr.whenTrue);
        this.write(' else ');
        this.writeExpression(expr.whenFalse);
    }

    protected writeLambdaExpression(expr: cs.LambdaExpression) {
        this._returnRunTest.push(false);

        if (
            expr.parameters.length === 0 &&
            cs.isPrimitiveTypeNode(expr.returnType) &&
            expr.returnType.type === cs.PrimitiveType.Void &&
            !cs.isBlock(expr.body)
        ) {
            this.beginBlock();
            this.writeExpression(expr.body);
            this.writeLine();
            this.endBlock();
        } else {
            this.write('fun(');
            this.writeCommaSeparated(expr.parameters, p => this.writeParameter(p));
            this.write('): ');
            this.writeType(expr.returnType);
            if (cs.isBlock(expr.body)) {
                this.writeBlock(expr.body);
            } else {
                this.beginBlock();
                this.write('return ');
                this.writeExpression(expr.body);
                this.writeLine();
                this.endBlock();
            }
        }
        this._returnRunTest.pop();
    }

    protected shouldWriteDoubleSuffix(expr: cs.Expression): boolean {
        let shouldWriteSuffix = false;
        if (!this._forceInteger && expr.parent) {
            shouldWriteSuffix = cs.isNumericLiteral(expr) ? expr.value.indexOf('.') === -1 : false;

            switch (expr.parent.nodeType) {
                case cs.SyntaxKind.ParenthesizedExpression:
                    shouldWriteSuffix = this.shouldWriteDoubleSuffix(expr.parent!.parent!);
                    break;
                case cs.SyntaxKind.PropertyDeclaration:
                case cs.SyntaxKind.FieldDeclaration:
                case cs.SyntaxKind.VariableDeclaration:
                case cs.SyntaxKind.ConditionalExpression:
                    break;
                case cs.SyntaxKind.PrefixUnaryExpression:
                    switch ((expr.parent as cs.PrefixUnaryExpression).operator) {
                        case '~':
                            shouldWriteSuffix = false;
                            break;
                    }
                    break;
                case cs.SyntaxKind.BinaryExpression:
                    const bin = expr.parent as cs.BinaryExpression;
                    switch (bin.operator) {
                        case '<<':
                        case '>>':
                        case '<':
                        case '>':
                        case '<=':
                        case '>=':
                        case '|':
                        case '^':
                        case '&':
                        case '|=':
                        case '^=':
                        case '&=':
                            shouldWriteSuffix = false;
                            break;
                        case '==':
                        case '!=':
                            const otherExpr = bin.left === expr ? bin.right : bin.left;
                            shouldWriteSuffix = !this.isIntResultExpression(otherExpr);
                            break;
                    }
                    break;
            }
        }

        return shouldWriteSuffix;
    }

    protected isIntResultExpression(expr: cs.Expression): boolean {
        if (cs.isInvocationExpression(expr)) {
            return this.isIntResultExpression(expr.expression);
        }

        if (cs.isBinaryExpression(expr)) {
            switch (expr.operator) {
                case '<<':
                case '>>':
                case '&':
                case '|':
                case '^':
                    return true;
            }
            return false;
        }
        if (cs.isParenthesizedExpression(expr)) {
            return this.isIntResultExpression(expr.expression);
        }
        return false;
    }

    protected writeNumericLiteral(expr: cs.NumericLiteral) {
        this.write(expr.value);

        if (!expr.value.endsWith('L') && this.shouldWriteDoubleSuffix(expr)) {
            this.write('.0');
        }
    }

    protected writeStringTemplateExpression(expr: cs.StringTemplateExpression) {
        this.write('"""');
        for (const c of expr.chunks) {
            if (cs.isStringLiteral(c)) {
                const escapedText = c.text;
                this.write(escapedText);
            } else {
                this.write('${(');
                this.writeExpression(c);
                this.write(').toTemplate()}');
            }
        }
        this.write('"""');
    }

    protected writeArrayCreationExpression(expr: cs.ArrayCreationExpression) {
        if (expr.type) {
            if (expr.values) {
                this.writeType(expr.type, true);
                // let elementType: cs.TypeNode | null = null;
                // if (cs.isArrayTypeNode(expr.type)) {
                //     elementType = expr.type.elementType;
                // } else if (
                //     cs.isTypeReference(expr.type) &&
                //     typeof expr.type.reference !== 'string' &&
                //     cs.isArrayTypeNode(expr.type.reference)
                // ) {
                //     elementType = expr.type.reference.elementType;
                // }

                // const type = elementType ? this._getContainerTypeName(elementType) : null;
                // this.write('alphaTab.collections.');
                // if (type) {
                //     this.write(type);
                //     this.write('List');
                // } else {
                //     this.write('List');
                //     if (expr.type && cs.isArrayTypeNode(expr.type)) {
                //         this.write('<');
                //         this.writeType(expr.type.elementType);
                //         this.write('>');
                //     } else {
                //         debugger;
                //     }
                // }
                this.writeLine('(');
                this._indent++;
                this.writeCommaSeparated(expr.values, v => {
                    if (expr.values!.length > 10) {
                        this.writeLine();
                    }
                    this.writeExpression(v);
                });
                this._indent--;
                this.writeLine(')');
            } else {
                this.writeType(expr.type, true);
                this.write('(');
                this.writeExpression(expr.sizeExpression!);
                this.write(')');
            }
        } else if (expr.values && expr.values.length > 0) {
            // TODO: check for typed array creation
            this.write('alphaTab.collections.');
            this.write('List(');
            this.writeCommaSeparated(expr.values, v => {
                if (expr.values!.length > 10) {
                    this.writeLine();
                }
                this.writeExpression(v);
            });
            this.write(')');
        } else {
            this._context.addCsNodeDiagnostics(expr, 'Unknown array type', ts.DiagnosticCategory.Error);
        }
    }

    protected writeMemberAccessExpression(expr: cs.MemberAccessExpression) {
        const isEmpty = cs.isIdentifier(expr.expression) && expr.expression.text.length === 0;
        if (!isEmpty) {
            this.writeExpression(expr.expression);
            if (expr.nullSafe) {
                this.write('?.');
            } else if (this.isMethodAsDelegate(expr)) {
                this.write('::');
            } else if (expr.tsSymbol && this._context.isStaticSymbol(expr.tsSymbol)) {
                this.write('.');
            } else if (cs.isTypeReference(expr.expression)) {
                this.write('.');
            } else if (this._isNullable(expr.expression)) {
                this.write('!!.');
            } else {
                this.write('.');
            }
        }
        const name = this._context.getSymbolName(expr) ?? expr.member;
        this.writeIdentifier(name);
    }

    private _isNullable(expr: cs.Expression): boolean {
        const parent = expr.parent;
        if (cs.isParenthesizedExpression(expr)) {
            return this._isNullable(expr.expression);
        }

        if (
            cs.isNonNullExpression(expr) ||
            cs.isIdentifier(expr) ||
            cs.isArrayCreationExpression(expr) ||
            cs.isNewExpression(expr) ||
            cs.isThisLiteral(expr) ||
            cs.isBaseLiteralExpression(expr) ||
            cs.isNumericLiteral(expr)
        ) {
            return false;
        }

        const tsNode = (expr as cs.Expression).tsNode;
        if (!tsNode) {
            return true;
        }

        if (parent && parent.tsNode?.kind === ts.SyntaxKind.AsExpression) {
            return false;
        }

        const symbol = this._context.typeChecker.getSymbolAtLocation(tsNode);
        let type: ts.Type;
        if (symbol && symbol.valueDeclaration) {
            if (
                (symbol.flags & ts.SymbolFlags.BlockScopedVariable) !== 0 ||
                (symbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0 ||
                (symbol.flags & ts.SymbolFlags.BlockScoped) !== 0
            ) {
                type = this._context.typeChecker.getTypeAtLocation(tsNode);
            } else {
                type = this._context.typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
            }
        } else {
            type = this._context.typeChecker.getTypeAtLocation(tsNode);
        }

        return this._context.isNullableType(type);
    }

    protected isMethodAsDelegate(expr: cs.MemberAccessExpression) {
        if (
            expr.tsSymbol?.valueDeclaration &&
            ts.isMethodDeclaration(expr.tsSymbol.valueDeclaration) &&
            (!ts.isCallExpression(expr.tsNode!.parent) ||
                (expr.tsNode!.parent as ts.CallExpression).expression !== expr.tsNode)
        ) {
            return true;
        }
        return false;
    }

    protected writeElementAccessExpression(expr: cs.ElementAccessExpression) {
        this.writeExpression(expr.expression);
        if (expr.nullSafe) {
            this.write('?.get(');
            this.writeExpression(expr.argumentExpression);
            this.write(')');
            return;
        }

        if (this._isNullable(expr.expression)) {
            this.write('!!');
        }
        this.write('[');
        this.writeExpression(expr.argumentExpression);
        this.write(']');
    }

    protected writeNewExpression(expr: cs.NewExpression) {
        this.writeType(expr.type, true);
        this.write('(');
        this.writeCommaSeparated(expr.arguments, a => this.writeExpression(a));
        this.write(')');
    }

    protected writeCastExpression(expr: cs.CastExpression) {
        if (cs.isPrimitiveTypeNode(expr.type)) {
            switch (expr.type.type) {
                case cs.PrimitiveType.String:
                    this.writeExpression(expr.expression);
                    this.write('.toString()');
                    return;
                case cs.PrimitiveType.Double:
                    this.writeExpression(expr.expression);
                    if (this._isNullable(expr.expression)) {
                        this.write('!!');
                    }
                    this.write('.toDouble()');
                    return;
                case cs.PrimitiveType.Int:
                    this.writeExpression(expr.expression);
                    if (this._isNullable(expr.expression)) {
                        this.write('!!');
                    }
                    this.write('.toInt()');
                    return;
                case cs.PrimitiveType.Long:
                    this.writeExpression(expr.expression);
                    if (this._isNullable(expr.expression)) {
                        this.write('!!');
                    }
                    this.write('.toLong()');
                    return;
            }
        }

        this.write('(');
        this.writeExpression(expr.expression);
        this.write(' as ');
        this.writeType(expr.type);
        this.write(')');
    }

    protected writeNonNullExpression(expr: cs.NonNullExpression) {
        this.writeExpression(expr.expression);
        if (!cs.isNonNullExpression(expr.expression)) {
            this.write('!!');
        }
    }

    private _catchVar: number = 0;
    protected writeCatchClause(c: cs.CatchClause): void {
        if (c.variableDeclaration) {
            this.write('catch (');
            this.writeIdentifier(c.variableDeclaration.name);
            this.write(': ');
            this.writeType(c.variableDeclaration.type);
            this.writeLine(')');
            this.writeBlock(c.block);
        } else {
            this.write('catch (');
            const varName = `_e${this._catchVar++}`;
            this.writeIdentifier(varName);
            this.write(': kotlin.Throwable)');
            this.writeBlock(c.block);
            this._catchVar--;
        }
    }

    protected writeSwitchStatement(s: cs.SwitchStatement) {
        this.write('when (');
        this.writeExpression(s.expression);
        this.writeLine(')');
        this.beginBlock();

        let hasDefault = false;

        for (let i = 0; i < s.caseClauses.length; i++) {
            const c = s.caseClauses[i];
            if (cs.isDefaultClause(c)) {
                hasDefault = true;
                this.writeDefaultClause(c as cs.DefaultClause);
            } else {
                // avoid "value", "value2", else ->
                // but write directly else ->
                let hasNonElseStatement = false;
                for (let j = i; j < s.caseClauses.length; j++) {
                    const c2 = s.caseClauses[j];
                    if (cs.isDefaultClause(c2)) {
                        break;
                    }

                    if ((c2 as cs.CaseClause).statements.length > 0) {
                        hasNonElseStatement = true;
                        break;
                    }
                }

                if (hasNonElseStatement) {
                    this.writeCaseClause(c as cs.CaseClause);
                }
            }
        }

        if (!hasDefault) {
            this.writeLine('else -> { }');
        }

        this.endBlock();
    }

    protected getCaseClauseStatements(stmt: cs.Statement[]): cs.Statement[] {
        if (stmt.length === 0) {
            return stmt;
        }
        if (cs.isBreakStatement(stmt[stmt.length - 1])) {
            stmt.pop();
        }

        if (stmt.length === 1 && cs.isBlock(stmt[0])) {
            stmt = (stmt[0] as cs.Block).statements;
        }
        stmt = stmt.slice();

        if (stmt.length > 0 && cs.isBreakStatement(stmt[stmt.length - 1])) {
            stmt.pop();
        }

        return stmt;
    }

    protected writeCaseClause(c: cs.CaseClause) {
        this.writeExpression(c.expression);
        if (c.statements.length === 0) {
            this.write(', ');
            return;
        }

        this.writeLine(' -> ');
        this.beginBlock();

        for (const s of this.getCaseClauseStatements(c.statements)) {
            this.writeStatement(s);
        }
        this.endBlock();
    }

    protected writeDefaultClause(c: cs.DefaultClause) {
        this.writeLine('else -> ');
        this.beginBlock();
        for (const s of this.getCaseClauseStatements(c.statements)) {
            this.writeStatement(s);
        }
        this.endBlock();
    }

    protected writeForEachStatement(s: cs.ForEachStatement) {
        this.write('for (');
        if (cs.isVariableDeclarationList(s.initializer)) {
            const d = s.initializer.declarations[0];
            if (d.deconstructNames) {
                this.write('(');
                d.deconstructNames.forEach((v, i) => {
                    if (i > 0) {
                        this.write(', ');
                    }
                    this.write(v);
                });
                this.write(')');
            } else {
                this.write(d.name);
            }
        } else {
            this.writeExpression(s.initializer as cs.Expression);
        }
        this.write(' in ');
        this.writeExpression(s.expression);
        this.writeLine(')');

        if (cs.isBlock(s.statement)) {
            this.writeStatement(s.statement);
        } else {
            this._indent++;
            this.writeStatement(s.statement);
            this._indent--;
        }
    }

    private _forLoopIncrementors: (cs.Expression | undefined)[] = [];

    protected writeForStatement(s: cs.ForStatement) {
        this._forLoopIncrementors.push(s.incrementor);

        // if (/*!this.tryWriteForRange(s)*/ true) {
        this.write('if (true) ');
        this.beginBlock();

        if (s.initializer) {
            if (cs.isVariableDeclarationList(s.initializer)) {
                this.writeVariableDeclarationList(s.initializer);
            } else {
                this.writeExpression(s.initializer as cs.Expression);
            }
        }
        this.writeLine();

        this.write('while(');
        if (s.condition) {
            this.writeExpression(s.condition);
        }
        this.write(')');
        this.beginBlock();

        if (cs.isBlock(s.statement)) {
            for (const stmt of s.statement.statements) {
                this.writeStatement(stmt);
            }
        } else {
            this._indent++;
            this.writeStatement(s.statement);
            this._indent--;
        }
        this._writeForIncrementors(this._forLoopIncrementors[this._forLoopIncrementors.length - 1]);
        this.endBlock();
        this.endBlock();
        // }

        this._forLoopIncrementors.pop();
    }
    private _writeForIncrementors(i: cs.Expression | undefined) {
        if (!i) {
            return;
        }

        if (cs.isBinaryExpression(i)) {
            this._writeBinaryExpressionsAsStatements(i);
        } else {
            this.writeExpression(i);
            this.writeLine();
        }
    }

    private _writeBinaryExpressionsAsStatements(e: cs.BinaryExpression) {
        if (e.operator === ',') {
            if (cs.isBinaryExpression(e.left)) {
                this._writeBinaryExpressionsAsStatements(e.left);
            } else {
                this.writeExpression(e.left);
                this.writeLine();
            }

            if (cs.isBinaryExpression(e.right)) {
                this._writeBinaryExpressionsAsStatements(e.right);
            } else {
                this.writeExpression(e.right);
                this.writeLine();
            }
        } else {
            this.writeBinaryExpression(e);
            this.writeLine();
        }
    }

    protected override writeContinueStatement(c: cs.ContinueStatement): void {
        this._writeForIncrementors(this._forLoopIncrementors[this._forLoopIncrementors.length - 1]);
        super.writeContinueStatement(c);
    }

    protected tryWriteForRange(s: cs.ForStatement): boolean {
        if (!s.initializer || !s.condition || !s.incrementor) {
            return false;
        }

        if (s.initializer.nodeType !== cs.SyntaxKind.VariableDeclarationList) {
            return false;
        }

        if (s.condition?.nodeType !== cs.SyntaxKind.BinaryExpression) {
            return false;
        }

        const writeIncrementor = () => {
            if (!s.incrementor) {
                return;
            }
            switch (s.incrementor.nodeType) {
                case cs.SyntaxKind.PrefixUnaryExpression:
                    const preOp = (s.incrementor as cs.PrefixUnaryExpression).operand;
                    if (preOp.nodeType !== cs.SyntaxKind.Identifier) {
                        this._context.addCsNodeDiagnostics(
                            s.incrementor,
                            'Unknown for incrementor',
                            ts.DiagnosticCategory.Error
                        );
                    } else {
                        switch ((s.incrementor as cs.PrefixUnaryExpression).operator) {
                            case '++':
                                this.write('it + 1');
                                break;
                            case '--':
                                this.write('it - 1');
                                break;
                            default:
                                this._context.addCsNodeDiagnostics(
                                    s.incrementor,
                                    'Unknown for incrementor',
                                    ts.DiagnosticCategory.Error
                                );
                                break;
                        }
                    }
                    break;
                case cs.SyntaxKind.PostfixUnaryExpression:
                    switch ((s.incrementor as cs.PostfixUnaryExpression).operator) {
                        case '++':
                            this.write('it + 1');
                            break;
                        case '--':
                            this.write('it - 1');
                            break;
                        default:
                            this._context.addCsNodeDiagnostics(
                                s.incrementor,
                                'Unknown incrementor',
                                ts.DiagnosticCategory.Error
                            );
                            break;
                    }
                    break;
                case cs.SyntaxKind.BinaryExpression:
                    switch ((s.incrementor as cs.BinaryExpression).operator) {
                        case '+=':
                            this.write('it + ');
                            this.writeExpression((s.incrementor as cs.BinaryExpression).right);
                            break;
                        case '-=':
                            this.write('it - ');
                            this.writeExpression((s.incrementor as cs.BinaryExpression).right);
                            break;
                        default:
                            this._context.addCsNodeDiagnostics(
                                s.incrementor,
                                'Unknown incrementor',
                                ts.DiagnosticCategory.Error
                            );
                            break;
                    }
                    break;
            }
        };

        const decl = s.initializer as cs.VariableDeclarationList;
        if (decl.declarations.length === 1) {
            const name = decl.declarations[0].name;
            const lower = decl.declarations[0].initializer;
            if (!lower) {
                return false;
            }

            const left = (s.condition as cs.BinaryExpression).left;
            const right = (s.condition as cs.BinaryExpression).right;
            if (cs.isIdentifier(left)) {
                if (left.text !== name) {
                    return false;
                }
            } else if (cs.isIdentifier(right)) {
                if (right.text !== name) {
                    return false;
                }
            } else {
                return false;
            }

            this.write(`for ( ${name} in generateSequence(`);
            this.writeExpression(lower);
            this.write(') { ');
            writeIncrementor();
            this.write(' }.takeWhile { ');

            if (cs.isIdentifier(left)) {
                this.write(' it ');
                this.write((s.condition as cs.BinaryExpression).operator);
                this.writeExpression((s.condition as cs.BinaryExpression).right);
            } else {
                this.writeExpression((s.condition as cs.BinaryExpression).left);
                this.write(` ${(s.condition as cs.BinaryExpression).operator}`);
                this.write(' it');
            }

            this.write(' }) ');
        } else if (decl.declarations.length === 2) {
            const lowerName = decl.declarations[0].name;
            const lower = decl.declarations[0].initializer;
            const upperName = decl.declarations[1].name;
            const upper = decl.declarations[1].initializer;
            if (!lower || !upper) {
                return false;
            }

            const left = (s.condition as cs.BinaryExpression).left;
            if (cs.isIdentifier(left)) {
                if (left.text !== lowerName) {
                    return false;
                }
            } else {
                return false;
            }

            const right = (s.condition as cs.BinaryExpression).left;
            if (cs.isIdentifier(right)) {
                if (right.text !== upperName) {
                    return false;
                }
            } else {
                return false;
            }

            this.write(`for( ${lowerName} in generateSequence( `);
            this.writeExpression(lower);
            this.write(') { ');
            writeIncrementor();
            this.write(' }.takeWhile { ');

            if ((left as cs.Identifier).text === lowerName) {
                this.write('it ');
                this.write((s.condition as cs.BinaryExpression).operator);
                this.writeExpression((s.condition as cs.BinaryExpression).right);
            } else {
                this.writeExpression((s.condition as cs.BinaryExpression).left);
                this.write((s.condition as cs.BinaryExpression).operator);
                this.write(' it');
            }

            this.write(' }) ');
        } else {
            return false;
        }

        this.beginBlock();

        if (cs.isBlock(s.statement)) {
            for (const stmt of s.statement.statements) {
                this.writeStatement(stmt);
            }
        } else {
            this.writeStatement(s.statement);
        }

        this.endBlock();
        return true;
    }

    protected override beginBlock() {
        super.beginBlock();
        this._useScopes.push(0);
    }

    protected override endBlock() {
        const useScopes = this._useScopes.pop();
        if (useScopes !== undefined) {
            for (let i = 0; i < useScopes; i++) {
                super.endBlock();
            }
        }

        super.endBlock();
    }

    protected writeVariableStatement(s: cs.VariableStatement) {
        switch (s.variableStatementKind) {
            case cs.VariableStatementKind.Normal:
            case cs.VariableStatementKind.Const:
                this.writeVariableDeclarationList(s.declarationList);
                break;
            case cs.VariableStatementKind.Using:
            case cs.VariableStatementKind.AwaitUsing:
                for (const declaration of s.declarationList.declarations) {
                    if (declaration.name === '_') {
                        this.write('(');
                        this.writeExpression(s.declarationList.declarations[0].initializer!);
                        this.writeLine(')');
                    } else {
                        this.writeVariableDeclaration(declaration);
                        this.write(declaration.name);
                    }

                    this.write('.use ');
                    super.beginBlock(); // base class is important to not start a separate scope!
                    const idx = this._useScopes.length - 1;
                    this._useScopes[idx] = this._useScopes[idx] + 1;
                }
                break;
        }
    }
    protected writeVariableDeclarationList(declarationList: cs.VariableDeclarationList) {
        for (const d of declarationList.declarations) {
            this.writeVariableDeclaration(d);
        }
    }
    protected writeVariableDeclaration(d: cs.VariableDeclaration) {
        this.write('var ');
        if (d.deconstructNames) {
            this.write('(');
            d.deconstructNames.forEach((v, i) => {
                if (i > 0) {
                    this.write(', ');
                }
                this.writeIdentifier(v);
            });
            this.write(')');
        } else {
            this.writeIdentifier(d.name);
        }

        if (
            d.type.nodeType !== cs.SyntaxKind.PrimitiveTypeNode ||
            (d.type as cs.PrimitiveTypeNode).type !== cs.PrimitiveType.Var
        ) {
            this.write(': ');
            this.writeType(d.type);
        }

        if (d.initializer) {
            this.write(' = ');
            this.writeExpression(d.initializer);
        }

        this.writeLine();
    }

    protected writeBlock(b: cs.Block) {
        if (b.parent && cs.isBlock(b.parent)) {
            this.write('if (true) ');
        }
        this.beginBlock();
        for (const s of b.statements) {
            this.writeStatement(s);
        }
        this.endBlock();
    }

    protected writeImport(using: cs.UsingDeclaration) {
        if (using.skipEmit) {
            return;
        }

        if (using.alias) {
            this.writeLine(`typalias ${using.name} = `);
            this.writeType(using.alias, true);
        } else {
            this.writeLine(`import ${using.name}.*`);
        }
    }

    protected override writeSemicolon() {
        this.writeLine();
    }

    protected override writeIsExpression(expr: cs.IsExpression) {
        if (expr.parent?.nodeType === cs.SyntaxKind.CaseClause) {
            this.write('(');
        }
        super.writeIsExpression(expr);
        if (expr.parent?.nodeType === cs.SyntaxKind.CaseClause) {
            this.write(')');
        }
    }

    protected override writeSpreadExpression(expr: cs.SpreadExpression) {
        this.write('*');
        this.writeExpression(expr.expression);
        if (
            expr.expression.tsSymbol?.valueDeclaration?.kind !== ts.SyntaxKind.Parameter ||
            (expr.expression.tsSymbol?.valueDeclaration as ts.ParameterDeclaration).dotDotDotToken === undefined
        ) {
            this.write('.spread()');
        }
    }

    protected writeLocalFunction(expr: cs.LocalFunctionDeclaration) {
        this.write(`fun ${expr.name}`);
        this.writeParameters(expr.parameters);
        this.write(': ');
        this.writeType(expr.returnType);
        this.writeBlock(expr.body);
    }

    protected writeYieldExpression(expr: cs.YieldExpression) {
        if (expr.expression) {
            this.write('yield(');
            this.writeExpression(expr.expression);
            this.write(')');
        } else {
            this.write('return');
        }
    }

    protected override writeDefaultExpression(d: cs.DefaultExpression): void {
        if (d.parent && cs.isParameterDeclaration(d.parent) && d.parent.type) {
            if (d.parent.type.isNullable) {
                this.write('null');
            } else if (cs.isPrimitiveTypeNode(d.parent.type)) {
                switch (d.parent.type.type) {
                    case cs.PrimitiveType.Bool:
                        this.write('false');
                        break;
                    case cs.PrimitiveType.Double:
                        this.write('0.0');
                        break;
                    case cs.PrimitiveType.Int:
                        this.write('0');
                        break;
                    case cs.PrimitiveType.Long:
                        this.write('0L');
                        break;
                    default:
                        this.write('null');
                        break;
                }
            } else if (cs.isArrayTupleNode(d.parent.type)) {
                this.writeType(d.parent.type, true);
                this.write('()');
            } else {
                this.write('null');
            }
        } else {
            this.write('null');
        }
    }

    protected override writeLabeledExpression(expr: cs.LabeledExpression) {
        this.write(expr.label);
        this.write(' = ');
        this.writeExpression(expr.expression);
    }

    protected writeDelegateDeclaration(d: cs.DelegateDeclaration) {
        this.writeDocumentation(d);

        this.writeLine('@OptIn(kotlin.contracts.ExperimentalContracts::class)');
        this.writeLine('@kotlin.ExperimentalUnsignedTypes');
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);
        this.write('typealias ');
        this.write(d.name);
        this.write(' = ');

        this.write('(');
        this.writeCommaSeparated(d.parameters, p => this.writeParameter(p));
        this.write(') -> ');
        this.writeType(d.returnType);
        this.writeLine();
    }

    protected writeDeconstructDeclaration(expr: cs.DeconstructDeclaration) {
        this.write('(');
        expr.names.forEach((v, i) => {
            if (i > 0) {
                this.write(', ');
            }
            this.write(this.escapeIdentifier(v));
        });
        this.write(')');
    }

    protected override writeInvocationExpression(expr: cs.InvocationExpression): void {
        if (expr.arguments.length === 1 && expr.arguments[0].nodeType === cs.SyntaxKind.Block) {
            this.writeExpression(expr.expression);
            if (expr.typeArguments) {
                this.write('<');
                this.writeCommaSeparated(expr.typeArguments, t => this.writeType(t));
                this.write('>');
            }
            this.writeBlock(expr.arguments[0] as cs.Block);
        } else {
            super.writeInvocationExpression(expr);
        }
    }
    protected override writeStringLiteral(expr: cs.StringLiteral): void {
        this.write(JSON.stringify(expr.text).replace('${', '\\$\\{'));
    }
}
